A vending machine is a self-service automated device that dispenses items like snacks, beverages, or other products to users without the need for human assistance. These machines are commonly found in places like offices, schools, airports, and train stations.
24/7 REFRESHMENTS
Users typically interact with the machine by:
In this chapter, we will explore the low level design of a vending machine in detail.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: Should the machine allow multiple transactions to run concurrently?
Interviewer: No, only one transaction should be allowed at a time.
Candidate: Should the vending machine support multiple payment methods, such as coins, notes, and digital payments?
Interviewer: For this version, let’s support only coin-based transactions. The machine should accept fixed denominations such as $1, $5, and $10.
Candidate: Should the machine be able to return change if the inserted amount exceeds the item price?
Interviewer: Yes, returning the correct change is an important part of the system.
Candidate: Can users cancel a transaction midway and get their money back?
Interviewer: Yes, users should be able to cancel a transaction any time before an item is dispensed. The full amount inserted should be refunded.
Candidate: Do we need to support admin operations such as restocking items or updating item prices?
Interviewer: The system should support restocking items or adding new items with a specified quantity. For simplicity, let's skip price updates for now.
Candidate: Do we need to track and log transaction history or sales data?
Interviewer: No need to maintain detailed transaction history. However, we can display intermediate states during a transaction
Candidate: Should the machine generate a receipt after the purchase?
Interviewer: No, receipt generation is not required for this version.
After gathering the details, we can summarize the key system requirements.
After the requirements are clear, lets identify the core entities/objects we will have in our system.
Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., item, coin, transaction, display, vending machine) and actions (e.g., select, dispense, refund, restock) from the functional requirements. These typically translate into classes, enums, or interfaces in an object-oriented design.
Below, we break down the functional requirements and extract the relevant entities. Related requirements are grouped together when they represent the same conceptual unit.
This introduces:
Coin (enum): Represents valid coin denominations accepted by the machine (e.g., $1, $5, $10). Using an enum helps enforce a fixed set of supported denominations and simplifies validation and change calculation.This introduces:
Item: Represents a product sold by the machine. Each item has attributes such as code, name, price, and quantity.Inventory: Manages the machine’s stock of items. It maintains a mapping of item codes to their corresponding Item objects and provides operations to add, restock, reduce, and check availability.This introduces:
VendingMachine: The central orchestrator that coordinates all user interactions and internal processes. It manages the current balance, selected item code, coin validation, transaction flow, state transitions, and interactions with the inventory.Item: Represents a product in the machine with attributes like code, name, price, and quantity.Coin (Enum): Defines the set of valid denominations accepted by the machine (e.g., $1, $5, $10).Inventory: Manages all items in the vending machine. Provides methods to add new items, restock inventory, check item availability, and reduce stock after a purchase.VendingMachine: Central class that handles coin insertion, item selection, inventory access, dispensing logic, change calculation, and state transitions.These core entities define the essential abstractions of the vending machine system and will guide the structure of our low-level design and class diagrams.
The system is composed of several types of classes, each with a distinct role.
Coin: Represents the set of valid coin denominations accepted by the machine. Using an enum ensures type safety and centralizes the value of each coin (e.g., PENNY(1), QUARTER(25)), making the system easy to extend with new coin types without changing the core logic.ItemA simple Plain Old Java Object (POJO) or data class that models a product. It holds product-specific information: a unique code for selection (e.g., "A1"), a name ("Coke"), and a price (in cents). This class has no business logic; its sole purpose is to encapsulate data.
InventoryThis class is responsible for managing the stock of all items.
It uses two maps: one to associate an item code with its Item object and another to track the quantity (stock) of each item. Its responsibilities are limited to adding items, checking availability, and reducing stock, adhering to the Single Responsibility Principle.
VendingMachine (The Context)This is the main class and the primary entry point for any client interaction.
It holds references to the current state (currentState), the Inventory, the current balance, and the selectedItemCode. It delegates all user actions to the current state object, which handles the request based on the machine's current context.
The relationships between classes define the system's structure and data flow.
VendingMachine has an Inventory): The VendingMachine owns the Inventory. The Inventory cannot exist without the VendingMachine, and its lifecycle is managed by the VendingMachine. This is a strong "has-a" relationship.Inventory has Items): The Inventory manages a collection of Item objects. While the inventory contains items, the Item objects themselves can be considered independent entities. This is a "has-a" relationship, but weaker than composition.VendingMachine has a VendingMachineState): The VendingMachine maintains a reference to its current state object. This reference can change dynamically at runtime, which is the essence of the State pattern. Furthermore, each VendingMachineState object holds a reference back to the VendingMachine to access its context and trigger state transitions.IdleState is a VendingMachineState): The concrete state classes (IdleState, ItemSelectedState, etc.) extend the abstract VendingMachineState class. This enforces a common contract across all states and allows the VendingMachine to treat them polymorphically.Several design patterns are employed to create a clean, maintainable, and extensible system.
This is the primary pattern used. It allows the VendingMachine to alter its behavior when its internal state changes.
The machine delegates requests to a state object, which implements the behavior for that specific state. This eliminates the need for large if/else or switch blocks for managing state-dependent logic, making the system cleaner and easier to modify.
VendingMachineVendingMachineState (abstract class)IdleState, ItemSelectedState, HasMoneyState, DispensingStateThe VendingMachine is implemented as a Singleton. This ensures that only one instance of the machine is created throughout the application's lifecycle. This is a logical choice as it models a real-world scenario where you interact with a single, physical machine.
The VendingMachine class acts as a Facade. It provides a simple, unified interface (insertCoin(), selectItem(), etc.) to the client. The client interacts with this simplified interface without needing to know about the complex internal subsystems like state management, inventory tracking, or state transition logic.
Coin EnumRepresents accepted coin denominations and their values (in cents).
1class Coin(Enum):
2 PENNY = 1
3 NICKEL = 5
4 DIME = 10
5 QUARTER = 25
6
7 def __init__(self, value: int):
8 self.value = value
9
10 def get_value(self) -> int:
11 return self.valueItemModels a product available for purchase in the vending machine.
1class Item:
2 def __init__(self, code: str, name: str, price: int):
3 self.code = code
4 self.name = name
5 self.price = price
6
7 def get_name(self) -> str:
8 return self.name
9
10 def get_price(self) -> int:
11 return self.priceEach item has a unique code, name, and price.
InventoryThis component is responsible for storing and tracking the available items and their quantities.
1class Inventory:
2 def __init__(self):
3 self.item_map: Dict[str, Item] = {}
4 self.stock_map: Dict[str, int] = {}
5
6 def add_item(self, code: str, item: Item, quantity: int) -> None:
7 self.item_map[code] = item
8 self.stock_map[code] = quantity
9
10 def get_item(self, code: str) -> Optional[Item]:
11 return self.item_map.get(code)
12
13 def is_available(self, code: str) -> bool:
14 return self.stock_map.get(code, 0) > 0
15
16 def reduce_stock(self, code: str) -> None:
17 self.stock_map[code] = self.stock_map[code] - 1The Inventory class has a single, clear purpose: to manage the collection of items and their stock levels. It is decoupled from the machine's operational logic (like handling money or state transitions).
addItem() registers new stock.reduceStock() decrements stock after dispensing.VendingMachineState Interface and Concrete StatesThe State pattern allows an object (the VendingMachine) to change its behavior when its internal state changes. The object appears to change its class.
VendingMachineStateThis defines the contract for all possible states.
1class VendingMachineState(ABC):
2 def __init__(self, machine):
3 self.machine = machine
4
5 @abstractmethod
6 def insert_coin(self, coin: Coin) -> None:
7 pass
8
9 @abstractmethod
10 def select_item(self, code: str) -> None:
11 pass
12
13 @abstractmethod
14 def dispense(self) -> None:
15 pass
16
17 @abstractmethod
18 def refund(self) -> None:
19 passEach class represents a specific state of the vending machine and implements the behavior appropriate for that state.
The default state when the machine is waiting for a user to begin an interaction.
1class IdleState(VendingMachineState):
2 def __init__(self, machine):
3 super().__init__(machine)
4
5 def insert_coin(self, coin: Coin) -> None:
6 print("Please select an item before inserting money.")
7
8 def select_item(self, code: str) -> None:
9 if not self.machine.get_inventory().is_available(code):
10 print("Item not available.")
11 return
12 self.machine.set_selected_item_code(code)
13 self.machine.set_state(ItemSelectedState(self.machine))
14 print(f"Item selected: {code}")
15
16 def dispense(self) -> None:
17 print("No item selected.")
18
19 def refund(self) -> None:
20 print("No money to refund.")In the IdleState, only selectItem is a valid action. All other actions, like insertCoin or dispense, are invalid and result in an error message. A successful selection triggers a state transition to ItemSelectedState.
The state after a user has selected an item, and the machine is waiting for money.
1class ItemSelectedState(VendingMachineState):
2 def __init__(self, machine):
3 super().__init__(machine)
4
5 def insert_coin(self, coin: Coin) -> None:
6 self.machine.add_balance(coin.get_value())
7 print(f"Coin Inserted: {coin.get_value()}")
8 price = self.machine.get_selected_item().get_price()
9 if self.machine.get_balance() >= price:
10 print("Sufficient money received.")
11 self.machine.set_state(HasMoneyState(self.machine))
12
13 def select_item(self, code: str) -> None:
14 print("Item already selected.")
15
16 def dispense(self) -> None:
17 print("Please insert sufficient money.")
18
19 def refund(self) -> None:
20 self.machine.reset()
21 self.machine.set_state(IdleState(self.machine))In this state, the primary valid action is insertCoin. The state keeps track of the inserted money and, upon receiving sufficient funds, transitions the machine to the HasMoneyState.
The state when the machine has received enough money for the selected item and is ready to dispense.
1class HasMoneyState(VendingMachineState):
2 def __init__(self, machine):
3 super().__init__(machine)
4
5 def insert_coin(self, coin: Coin) -> None:
6 print("Already received full amount.")
7
8 def select_item(self, code: str) -> None:
9 print("Item already selected.")
10
11 def dispense(self) -> None:
12 self.machine.set_state(DispensingState(self.machine))
13 self.machine.dispense_item()
14
15 def refund(self) -> None:
16 self.machine.refund_balance()
17 self.machine.reset()
18 self.machine.set_state(IdleState(self.machine))
19he only valid action from the user's perspective is dispense. This action immediately transitions the machine to the DispensingState to prevent any other user interactions during the physical dispensing process.
A transient state that locks the machine while an item is being physically dispensed.
1class DispensingState(VendingMachineState):
2 def __init__(self, machine):
3 super().__init__(machine)
4
5 def insert_coin(self, coin: Coin) -> None:
6 print("Currently dispensing. Please wait.")
7
8 def select_item(self, code: str) -> None:
9 print("Currently dispensing. Please wait.")
10
11 def dispense(self) -> None:
12 # already triggered by HasMoneyState
13 pass
14
15 def refund(self) -> None:
16 print("Dispensing in progress. Refund not allowed.")This state effectively blocks all user input. The actual dispensing logic is handled by the VendingMachine's dispenseItem method, which, upon completion, will transition the machine back to the IdleState.
VendingMachine Class (Context)This class is the main entry point for all client interactions. It holds the current state and delegates actions to it.
1class VendingMachine:
2 _instance = None
3
4 def __new__(cls):
5 if cls._instance is None:
6 cls._instance = super(VendingMachine, cls).__new__(cls)
7 cls._instance._initialized = False
8 return cls._instance
9
10 def __init__(self):
11 if not hasattr(self, '_initialized') or not self._initialized:
12 self.inventory = Inventory()
13 self.current_state = IdleState(self)
14 self.balance = 0
15 self.selected_item_code = None
16 self._initialized = True
17
18 @classmethod
19 def get_instance(cls):
20 return cls()
21
22 def insert_coin(self, coin: Coin) -> None:
23 self.current_state.insert_coin(coin)
24
25 def add_item(self, code: str, name: str, price: int, quantity: int) -> Item:
26 item = Item(code, name, price)
27 self.inventory.add_item(code, item, quantity)
28 return item
29
30 def select_item(self, code: str) -> None:
31 self.current_state.select_item(code)
32
33 def dispense(self) -> None:
34 self.current_state.dispense()
35
36 def dispense_item(self) -> None:
37 item = self.inventory.get_item(self.selected_item_code)
38 if self.balance >= item.get_price():
39 self.inventory.reduce_stock(self.selected_item_code)
40 self.balance -= item.get_price()
41 print(f"Dispensed: {item.get_name()}")
42 if self.balance > 0:
43 print(f"Returning change: {self.balance}")
44 self.reset()
45 self.set_state(IdleState(self))
46
47 def refund_balance(self) -> None:
48 print(f"Refunding: {self.balance}")
49 self.balance = 0
50
51 def reset(self) -> None:
52 self.selected_item_code = None
53 self.balance = 0
54
55 def add_balance(self, value: int) -> None:
56 self.balance += value
57
58 def get_selected_item(self) -> Item:
59 return self.inventory.get_item(self.selected_item_code)
60
61 def set_selected_item_code(self, code: str) -> None:
62 self.selected_item_code = code
63
64 def set_state(self, state: VendingMachineState) -> None:
65 self.current_state = state
66
67 # Getters for states and inventory
68 def get_inventory(self) -> Inventory:
69 return self.inventory
70
71 def get_balance(self) -> int:
72 return self.balanceVendingMachineDemoThis driver class demonstrates a complete user journey, showcasing the state transitions in action.
1class VendingMachineDemo:
2 @staticmethod
3 def main():
4 vending_machine = VendingMachine.get_instance()
5
6 # Add products to the inventory
7 vending_machine.add_item("A1", "Coke", 25, 3)
8 vending_machine.add_item("A2", "Pepsi", 25, 2)
9 vending_machine.add_item("B1", "Water", 10, 5)
10
11 # Select a product
12 print("\n--- Step 1: Select an item ---")
13 vending_machine.select_item("A1")
14
15 # Insert coins
16 print("\n--- Step 2: Insert coins ---")
17 vending_machine.insert_coin(Coin.DIME) # 10
18 vending_machine.insert_coin(Coin.DIME) # 10
19 vending_machine.insert_coin(Coin.NICKEL) # 5
20
21 # Dispense the product
22 print("\n--- Step 3: Dispense item ---")
23 vending_machine.dispense() # Should dispense Coke
24
25 # Select another item
26 print("\n--- Step 4: Select another item ---")
27 vending_machine.select_item("B1")
28
29 # Insert more amount
30 print("\n--- Step 5: Insert more than needed ---")
31 vending_machine.insert_coin(Coin.QUARTER) # 25
32
33 # Try to dispense the product
34 print("\n--- Step 6: Dispense and return change ---")
35 vending_machine.dispense()
36
37
38if __name__ == "__main__":
39 VendingMachineDemo.main()Which entity manages the stock of items in a vending machine design?
No comments yet. Be the first to comment!